package fr.neyb.bernouilli.common.math;

import fr.neyb.bernouilli.common.exception.MathLimitExceededException;
import fr.neyb.bernouilli.common.exception.MathRuntimeException;
import fr.neyb.bernouilli.common.tools.BinaryTools;

public class MyMath {

	private MyMath() {
	}

	public static boolean isPalindromic(long l) {
		String s = Long.toString(l);

		for (int i = 0; i < s.length(); i++)
			if (s.charAt(i) != s.charAt(s.length() - (i + 1)))
				return false;

		return true;
	}

	public static long factorial(int i) {
		long res = 1;

		for (int j = 1; j <= i; j++) {
			res *= j;
		}

		return res;
	}

	public static long comb(int k, int n) {
		return (factorial(n)) / (factorial(k) * factorial(n - k));
	}

	public static long pow(long v, int power) {

		if (power < 0) {
			throw new fr.neyb.bernouilli.common.exception.MathRuntimeException(
					"cannot raise an integral value to a negative power ({0}^{1})",
					v, power);
		}

		long result = 1l;
		long k2p = v;
		while (power != 0) {
			if ((power & 0x1) != 0) {
				result *= k2p;
			}
			k2p *= k2p;
			power = power >> 1;
		}
		return result;
	}

	public static int i_sqrt(long l) {
		// FIXME this method suxx
		if (l < 0)
			throw new MathRuntimeException(
					"can't calculate square root of {0} : it is negative", l);

		Fraction prevResult = new Fraction(0);
		double prec;
		Fraction result = new Fraction(rough_sqrt(l));
		Fraction square = new Fraction(l);
		Fraction half = new Fraction(1, 2);

		while ((prec = Math.abs(result.toDouble() - prevResult.toDouble())) >= 0.5) {
			prevResult = new Fraction(result);
			try {
				result.add(new Fraction(square).divide(result)).multiply(half);
			} catch (MathLimitExceededException e) {
				result = new Fraction(prevResult).approximate();
				result.add(new Fraction(square).divide(result)).multiply(half);
			}
		}

		int v = (int) Math.floor(result.toDouble() - prec);

		for (int i = 0; i < 3; i++)
			if (isISqrt(l, v + i))
				return v + i;

		throw new MathRuntimeException(
				"probleme d'approximation : sqrt({0})~{1}", l, result);
	}

	public static long PGCD(long v1, long v2) {
		v1 = Math.abs(v1);
		v2 = Math.abs(v2);

		if (v1 == 0 && v2 == 0)
			throw new MathRuntimeException();

		if (v1 == 0)
			return v2;

		if (v2 == 0)
			return v1;

		long big;
		long small;
		long tmp;
		if (v1 > v2) {
			big = v1;
			small = v2;
		} else {
			big = v2;
			small = v1;
		}

		while (small != 0) {
			tmp = small;
			small = big % small;
			big = tmp;
		}

		return big;
	}

	public static long PPCM(long v1, long v2) {
		v1 = Math.abs(v1);
		v2 = Math.abs(v2);

		if (v1 == 0 || v2 == 0)
			throw new MathRuntimeException();

		if (v1 == 1)
			return v2;

		if (v2 == 1)
			return v1;

		long pgcd = PGCD(v1, v2);

		return v1 * (v2 / pgcd);
	}

	public static long checkSum(long l1, long l2) {
		if ((l1 > 0 && Long.MAX_VALUE - l1 > l2)
				|| (l1 < 0 && Long.MIN_VALUE - l1 < l2))
			throw new MathLimitExceededException();
		return l1 + l2;
	}

	public static long checkSub(long l1, long l2) {
		return checkSum(l1, -1 * l2);
	}

	public static long checkedMult(long l1, long l2) {
		if ((l1 > 0 && Long.MAX_VALUE / l1 < l2)
				|| (l1 < 0 && Long.MAX_VALUE / l1 > l2))
			throw new MathLimitExceededException("{0}x{1} is bigger than "
					+ Long.MAX_VALUE, l1, l2);

		return l1 * l2;
	}

	public static long squaresSumFirst(int n) {
		return n * (n + 1) * (2 * n + 1) / 6;
	}

	public static long sumSquareFirst(int n) {
		long res = n * (n + 1) / 2;
		return res * res;
	}

	private static boolean isISqrt(long square, int isqrt) {
		if (square < 0)
			return false;

		if (isqrt == 0)
			return square == 0;

		isqrt = Math.abs(isqrt);
		return (isqrt - 1) * (isqrt - 1) < square && isqrt * isqrt >= square;
	}

	private static long rough_sqrt(long l) {
		if (l == 0)
			return 0;
		return 1 << (BinaryTools.binaryDigitsNum(l) / 2 + 1);
	}
}
